package project3;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

import project3.Util.CellType;

public class IntermediateRouter {
	protected static Map<Integer, Integer> routingTable;
	private static Map<Integer, Integer> rroutingTable;
	private static Map<Integer, Socket> inSocket;
	private static Map<Integer, Socket> outSocket;
	private static ServerSocket socket;
	private static Random ran;
	
	public IntermediateRouter(int port) throws Exception {
		if (routingTable == null) {
			routingTable = new ConcurrentHashMap<Integer, Integer>();
			rroutingTable = new ConcurrentHashMap<Integer, Integer>();
			inSocket = new ConcurrentHashMap<Integer, Socket>();
			outSocket = new ConcurrentHashMap<Integer, Socket>();
			socket = new ServerSocket(port);
			ran = new Random();
			new ConcurrentHashMap<Socket, BlockingQueue<Cell>>();
		}
	}
	
	public void clear() {
		try {
			socket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void listenRouterRequest() { 
		new Thread() {
			
			@Override
			public void run() {
				while (true) {
					try {
						Socket soc = socket.accept();
						System.out.println("[IMRouter] New TCP Conenction in. ");
						Router.addNewSocket(soc);
					} catch (IOException e) {
						//TODO: Handle the error when browser connection to the server error
					}
				}
			}
		}.start();
	}
	
	/**
	 * Change a cell's circuit id field to routingTable.get(destCircId)
	 * @param srcCircId
	 * @param srcCell
	 * @return
	 */
	public static Cell setMappedCircuitId (int srcCircId, Cell srcCell) {
		// TODO: handle the case when srcCircId is the last hop on a circuit (i.e. end router case)
		int destCircId = routingTable.get(srcCircId);
		srcCell.setCircId(destCircId);
		
		return srcCell;
	}
	
	/**
	 * Put cell to the WriterThread buffer corresponsing to the cell's circuit field
	 * @param cell
	 */
	public static void putToMappedCircuitBuffer (Cell cell) {
		int destCircId = cell.getCircId();
		Socket writeToSocket;
		BlockingQueue<Cell> buffer;
		writeToSocket = outSocket.get(destCircId);
		buffer = Router.getOutBlockingQueue(writeToSocket);	
		
		boolean isFull = !buffer.offer(cell);
		if (isFull) 
			System.out.println("[IntermediateRouter] WARNING: buffer for circuit " + destCircId +
					" at Socket " + writeToSocket + " is full.");		
	}
	
	/**
	 * Returns true iff routing table indicates that circId is the last hop on the circuit 
	 * @param circId
	 * @return
	 */
	public static boolean isEndRouter (int circId) {
		return !routingTable.containsKey(circId);
	}
	
	/**
	 * Destroy the given circuit.
	 * 
	 * @param circId
	 */
	public static void destroyCircuit(int circId) {
		Cell cell = new DestroyCell(routingTable.get(circId), CellType.DESTROY);
		Router.sendMassageToRouter(outSocket.get(circId), cell);
		outSocket.remove(routingTable.get(circId));
		rroutingTable.remove(routingTable.get(circId));
		routingTable.remove(circId);
		inSocket.remove(circId);
	}
	
	/**
	 * Respond to the create message
	 * 
	 * @param curCell
	 */
	public static void respondToOpen(OpenCell curCell, Socket soc) {
		long fromAgent = curCell.getFromAgentId();
		long toAgent = curCell.getToAgentId();
		if (toAgent == Router.currentRouterId) {
			if (Router.isInCreation(fromAgent)) {
				OpenCell ret = new OpenCell(curCell.getCircId(), CellType.OPEN_FAILED);
				ret.setFromAgentId(fromAgent);
				ret.setToAgentId(toAgent);
				try {
					Router.sendMassageToRouter(soc, ret);
				} catch (Exception e) {
					// TODO: Handle the error when failing to send the message.
					System.out.println("[IMRouter] Fail to send open failed message back.");
				}
			} else {
				OpenCell ret = new OpenCell(curCell.getCircId(), CellType.OPENED);
				ret.setFromAgentId(curCell.getFromAgentId());
				ret.setToAgentId(curCell.getToAgentId());
				try {
					Router.sendMassageToRouter(soc, ret);
				} catch (Exception e) {
					// TODO: Handle the error when failing to send the message
					e.printStackTrace();
					System.out.println("[IMRouter] Fail to send opened message back.");
					
				}
			}
		} else {
			// TODO: Handle this impossible case.
			System.out.println("[IMRouter] Unknown Error");
		}
	}
	
	/**
	 * Respond to the create message
	 */
	public static void respondToCreate(Cell curcell, Socket soc) {
		if (routingTable.containsKey(curcell.circId)) {
			Router.sendMassageToRouter(soc, new CreateCell(curcell.circId, CellType.CREATE_FAILED));
		} else {
			inSocket.put(curcell.getCircId(), soc);
			Router.sendMassageToRouter(soc, new CreateCell(curcell.circId, CellType.CREATED));		
		}
	}

	public static void createHop(RelayCell curRelayCell, Socket socket) {
		// TODO Auto-generated method stub
		final int cirid = curRelayCell.getCircId();
		if (!inSocket.containsKey(cirid)) {
			// TODO: This will never happen, but need to handle this possible case.
			System.out.println("[IMRouter] Errrrrr, Unknown error happens. ");
		}
		if (routingTable.containsKey(cirid)) {
			curRelayCell.setCircId(routingTable.get(cirid));
			Router.sendMassageToRouter(outSocket.get(routingTable.get(cirid)), curRelayCell);
			new Thread() {
				@Override
				public void run() {
					RelayCell cell = (RelayCell) Router.getWantedCell(routingTable.get(cirid));
					int cmdType = cell.getRelayCmdType();
					if (cmdType == 0x04 || cmdType == 0x07) {
						cell.setCircId(cirid);
						Router.sendMassageToRouter(inSocket.get(cirid), cell);
					} else {
						Router.sendMassageToRouter(inSocket.get(cirid), new RelayCell(cirid, cell.getStreamId(), 0, cell.getRelayCmdType(), null));
					}
				}
			}.start();
		} else {
			// Not in the routing table yet, need to send create message.
			try {
				sendCreate(curRelayCell, cirid, 5, socket);
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	private static void sendCreate(final RelayCell curRelayCell, final int incirid, final int maxtimes, final Socket socket) throws Exception{
		int tempoutcircID = ran.nextInt(65536);
		while (routingTable.values().contains(tempoutcircID)) {
			tempoutcircID = ran.nextInt(65536);
		}
		final int outcircID = tempoutcircID;
		Router.sendMassageToRouter(socket, new CreateCell(outcircID, CellType.CREATE));
		new Thread() {
			@Override
			public void run() {
				if (Router.getWantedCell(outcircID).getCellType() == CellType.CREATED) {
					routingTable.put(incirid, outcircID);
					rroutingTable.put(outcircID, incirid);
					outSocket.put(outcircID, socket);
					Router.sendMassageToRouter(inSocket.get(incirid), new RelayCell(incirid, curRelayCell.getStreamId(), 0, (byte) 0x07, new byte[0]));
				} else {
					if (maxtimes == 0) {
						Router.sendMassageToRouter(inSocket.get(incirid), new RelayCell(incirid, curRelayCell.getStreamId(), 0, (byte) 0x0c, new byte[0]));
					} else {
						try {
							sendCreate(curRelayCell, incirid, maxtimes-1, socket);
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			}
		}.start();
	}

	public static void sendToNextRouter(RelayCell curRelayCell, Socket sock) {
		final int circId = curRelayCell.getCircId();
		curRelayCell.setCircId(routingTable.get(circId));
		Router.sendMassageToRouter(sock, curRelayCell);
		if (curRelayCell.getRelayCmdType() == 0x01) {
			new Thread() {
				@Override
				public void run() {
					RelayCell cell = (RelayCell) Router.getWantedCell(routingTable.get(circId));
					cell.setCircId(circId);
					Router.sendMassageToRouter(inSocket.get(circId), cell);
				}
			}.start();
		}
	}
	
	public static void sendDataBack(Cell c, int circuitId, boolean fromEnd) {
		Socket soc;
		if (!fromEnd) {
			if (circuitId == -rroutingTable.get(circuitId)) {
				RelayCell rc = (RelayCell) c;
				SourceRouter.sendBackToBrowser(rc.getBody(), rc.getStreamId(), rc.getCircId());
				return;
			}
			c.setCircId(rroutingTable.get(circuitId));
			soc = inSocket.get(rroutingTable.get(circuitId));
		} else {
			soc = inSocket.get(circuitId);
		}
		Router.sendMassageToRouter(soc, c);
	}

	public static boolean shoudSendBack(int curCellCircId, Socket sock) {
		boolean ret = outSocket.containsKey(curCellCircId) && outSocket.get(curCellCircId).equals(sock);
		return ret;
	}
	
	public static boolean shouldSendBacktoBrowser(int circuitId, Socket sock) {
		boolean isSourceRouter = SourceRouter.isInSourceRouter(circuitId);
		boolean ret = isSourceRouter && outSocket.containsKey(circuitId) && outSocket.get(circuitId).equals(sock);
		return ret;
	}

	public static void registerBrowser(int circId, Socket socket) {
		// TODO Auto-generated method stub
		routingTable.put(-circId, circId);
		rroutingTable.put(circId, -circId);
		outSocket.put(circId, Router.getTcpConn(SourceRouter.getRouterIpPort(circId)));
		inSocket.put(-circId, socket);
	}
}
